gh-145040: Fix crash in sqlite3 when connection is closed from within a callback#145041
gh-145040: Fix crash in sqlite3 when connection is closed from within a callback#145041raminfp wants to merge 8 commits intopython:mainfrom
Conversation
…g aggregate callback Fix a segmentation fault in the _sqlite module that occurs when Connection.close() is called inside an aggregate step() callback. After stmt_step() returns, _pysqlite_query_execute() calls sqlite3_last_insert_rowid(self->connection->db) without checking if self->connection->db is still valid. If the connection was closed during the callback, self->connection->db is NULL, causing a NULL pointer dereference. The fix adds a NULL check for self->connection->db after stmt_step() returns, raising ProgrammingError instead of crashing.
Misc/NEWS.d/next/Library/2026-02-20-00-00-00.gh-issue-145040.sqlite3-close-crash.rst
Outdated
Show resolved
Hide resolved
…qlite3-close-crash.rst Co-authored-by: Benedikt Johannes <benedikt.johannes.hofer@gmail.com>
|
Please update the PR title to more accurately reflect the problem it addresses. Ditto for the NEWS item. |
…within a callback
4305a82 to
05086f0
Compare
|
Sorry for force push!!! |
|
This PR adds a sanity check after the damage has been done. The SQLite C API clearly tells us what's legal and illegal operations on the |
…lback Instead of detecting a closed connection after the damage has been done, prevent Connection.close() from succeeding while a SQLite callback is executing. This aligns with the SQLite C API docs, which state that applications must not close the database connection from within a callback. Add an in_callback counter to the connection object, incremented before stmt_step() and decremented after. If close() is called while the counter is positive, ProgrammingError is raised and the database connection remains open. A counter (rather than a boolean flag) is used to correctly handle nested callbacks. Also convert test docstrings to comments per reviewer feedback, and add a test for the nested callback scenario.
|
I agree, preventing the misuse upfront is the better approach. I've pushed a new commit that does exactly that:
The complexity turned out to be quite manageable, just a few lines in close(), and |
…ted callbacks Only check and consume the close_attempted_in_callback flag when in_callback reaches zero (the outermost level). Previously, a nested stmt_step() inside a callback could consume the flag, causing the outermost caller to miss the error.
|
We should be able to exploit the already present |
I did a quick PoC, and this indeed works. It's a small change, and it works for both the iternext and the query execute scenarios. However, it requires we roll back parts of 74c1f41. If we choose this path, we should do the rolling back as a separate PR first. |
|
@erlend-aasland I implemented your suggested approach. Here's what I did: Removed in_callback counter, now using
All 509 test_sqlite3 tests pass. |
…tion in callback Replace the in_callback counter with cursor->locked checks. Restore the cursor weakref list (partially rolling back 74c1f41) so that close() can iterate cursors and detect locked ones.
|
@raminfp, great. I'll be able to take a look at this later today (CET). |
| #include "pycore_pyerrors.h" // _PyErr_ChainExceptions1() | ||
| #include "pycore_pylifecycle.h" // _Py_IsInterpreterFinalizing() | ||
| #include "pycore_unicodeobject.h" // _PyUnicode_AsUTF8NoNUL | ||
| #include "pycore_weakref.h" |
There was a problem hiding this comment.
I don't think we need pycore_weakref.h; we should be fine with the public API functions here.
There was a problem hiding this comment.
See blob.c for how to iterate through the weak refs using the public API.
| self->statement_cache = statement_cache; | ||
| self->cursors = cursors; | ||
| self->blobs = blobs; | ||
| self->close_attempted_in_callback = 0; |
There was a problem hiding this comment.
Instead of adding this flag, did you try to just raise in pysqlite_connection_close_impl?
There was a problem hiding this comment.
(The exception should propagate through the existing (slightly weird) callback exception handling in connection.c)
|
Thank you, now in Iran, the internet is cut off for us people. netblocks. I use DNS tunnel is very slow. We are now in a crucial period in our country's history for freedom. Please give me time to make these changes after freedom. |
|
@raminfp, stay safe! Don't worry about the PR; it can wait. |
Summary
Fix a segmentation fault (NULL pointer dereference) in the
_sqlitemodule that occurs whenConnection.close()is called inside an aggregatestep()callback.Fix
stmt_step(), check ifself->connection->db == NULL. If so, raiseProgrammingError("Cannot operate on a closed database.")and jump to the error path.sqlite3_last_insert_rowid()call withself->connection->db != NULLas an extra safety net.Test
Added
test_aggr_close_conn_in_stepinLib/test/test_sqlite3/test_userfunctions.pythat reproduces the exact crash scenario and verifies thatProgrammingErroris raised instead of a segfault.ASAN output (before fix)
After fix
_sqlite: NULL dereference when connection is closed from within a callback #145040